# --------------------------------------------------------------------------------
#  NoiseTube Web application
#  
#  Copyright (C) 2008-2010 SONY Computer Science Laboratory Paris
#  Portions contributed by Vrije Universiteit Brussel (BrusSense team), 2008-2011
# --------------------------------------------------------------------------------
#  This library is free software; you can redistribute it and/or modify it under
#  the terms of the GNU Lesser General Public License, version 2.1, as published
#  by the Free Software Foundation.
#  
#  This library is distributed in the hope that it will be useful, but WITHOUT
#  ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
#  FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
#  details.
#  
#  You should have received a copy of the GNU Lesser General Public License along
#  with this library; if not, write to:
#    Free Software Foundation, Inc.,
#    51 Franklin Street, Fifth Floor,
#    Boston, MA  02110-1301, USA.
#  
#  Full GNU LGPL v2.1 text: http://www.gnu.org/licenses/old-licenses/lgpl-2.1.txt
#  NoiseTube project source code repository: http://code.google.com/p/noisetube
# --------------------------------------------------------------------------------
#  More information:
#   - NoiseTube project website: http://www.noisetube.net
#   - Sony Computer Science Laboratory Paris: http://csl.sony.fr
#   - VUB BrusSense team: http://www.brussense.be
# --------------------------------------------------------------------------------
 
require 'geo_features.rb'
require 'automatic_tagging.rb'

class Track < ActiveRecord::Base
  self.rgeo_factory_generator = RGeo::Geos.factory_generator(:srid => 4326)
  #set_rgeo_factory_for_column(:geom, RGeo::Geographic.spherical_factory(:srid => 4326))

  #acts_as_geom :geom

  belongs_to :user
  belongs_to :city
  has_many :measures #, :dependent => :destroy
  has_many :measurements_tags, :foreign_key => :track_id #, :through => :measures
  has_many :overlays

  validates_presence_of  :user
  # #validates_format_of :graph_image_url, :with => /http\:/ , :allow_nil=>true,
  # :allow_blank=>true

  attr_accessible :graph_image_url, :user, :city

  LEAST_MEASURES_SIZE_PER_TRACK = 30

  # Create a track from a xml
  def self.from_xml (xml_doc, user)
    # get the rootnode
    root = REXML::XPath.first( xml_doc, "//NoiseTube-Mobile-Session")

    # TODO if attribute "userKey" is present on the rootNode it's contents
    # should be matched with the API key of the current user

    # make a new track and set attributes...
    track = Track.new(:user=>user)
    track.starts_at = DateTime.parse(root.attribute("startTime").value)
    track.client = root.attribute("client").value  #TODO do a lookup/insert in the clients table and set the client_id field instead of using this string field
    track.client_version = root.attribute("clientVersion").value  unless root.attribute("clientVersion").nil?
    if (!root.attribute("deviceBrand").nil? && root.attribute("deviceBrand").value != "" && !root.attribute("deviceModel").nil? && root.attribute("deviceModel").value != "")
      track.device = root.attribute("deviceBrand").value + " " + root.attribute("deviceModel").value + (((!root.attribute("deviceModelVersion").nil?) && root.attribute("deviceModelVersion").value != "") ? " v" + root.attribute("deviceModelVersion").value : "") + (((!root.attribute("devicePlatform").nil?) && root.attribute("devicePlatform").value != "") ? " (" + root.attribute("devicePlatform").value + (((!root.attribute("devicePlatformVersion").nil?) && root.attribute("devicePlatformVersion").value != "") ? " " + root.attribute("devicePlatformVersion").value : "") + ")" : "")
    else
      track.device = ("J2ME string: " + root.attribute("deviceJ2MEPlatform").value) if ((!root.attribute("deviceJ2MEPlatform").nil?) && root.attribute("deviceJ2MEPlatform").value != "" && root.attribute("deviceJ2MEPlatform").value != "j2me")
    end
    track.track_comment = "Uploaded through website. calibration: " <<  root.attribute("calibration").try(:value)
    track.save

    # process all the measurements - build sql is faster than build objects
    #  xml example: <measurement timeStamp="2009-05-14T19:14:50+02:00" loudness="21" location="" tags="car"/>
    logger.info "----- New track by uploding data ------"
    puts "New track by uploding data"
    measures_insert_sql_start = "INSERT INTO measures(track_id,geom,loudness,loudness_index,created_at,made_at) VALUES "
    measures_insert_sql_values = []

    tags_insert_sql_start = "INSERT INTO measurements_tags(track_id,tag_name,user_id,context,begin_id,end_id,created_at) VALUES "
    tags_insert_sql_values = []
    #location missing in phones without GIS
    missing_locations_count = 0
    REXML::XPath.each( xml_doc, "//measurement" ) { |measurement|

      params={}
      params[:time] = measurement.attribute("timeStamp").value #is being parsed in measure.save_request
      params[:db]  = measurement.attribute("loudness").value.to_f if !measurement.attribute("loudness").nil?
      params[:l]  = measurement.attribute("location").value if !measurement.attribute("location").nil?
      params[:tag]  = measurement.attribute("tags").value if !measurement.attribute("tags").nil?


      loctext =  Measure.wkt_from_loc(params[:l])
      if loctext.blank?
        missing_locations_count = missing_locations_count + 1
        loctext = "NULL"
      else
        loctext = "'#{loctext}'"
      end


      #for tags, can be comma-separated..
      unless params[:tag].blank?
        #need to save the measurement - to get the id that links it to the tag
        nm = Measure.new()
        nm.track = track
        nm.geom =  loctext.gsub("'", "")    #strip quotes
        nm.loudness = params[:db]
        nm.loudness_index = SoundLevel.compute_loudness_index(params[:db])
        nm.created_at =  Time.now
        nm.made_at = params[:time]
        nm.user = user   # this will be removed
        nm.save!

        the_tags = params[:tag].split(",")
        the_tags.each{|ptag|
        tags_insert_sql_values << "(#{track.id}, #{sanitize(ptag)}, #{track.user_id}, 'tags', #{nm.id}, -1, '#{params[:time]}') "  #todo: tag intervals?
          }
      else
        #no tags - batch the save of the measure
        measures_insert_sql_values << "(#{track.id}, #{loctext}, #{params[:db]}, #{SoundLevel.compute_loudness_index(params[:db])}, '#{Time.now}', '#{params[:time]}') "
      end

      #track.add_measurement(params)
    }
    #now save the measurements as a batch
    Measure.save_as_transactions(measures_insert_sql_start, measures_insert_sql_values) unless measures_insert_sql_values.blank?
    MeasurementsTag.save_tags_as_transactions(tags_insert_sql_start, tags_insert_sql_values) unless tags_insert_sql_values.blank?

    track.ends_at = track.measures.find(:last, :order=>"made_at").try(:made_at)
    track.save
    if missing_locations_count > 10
      logger.warn("Measurements created at #{Time.now} on track #{track.id} contained #{missing_locations_count} measurements without GIS data.")
      end
    return track
  end

  def add_measurement(param)
    Measure.save_request(param, self, self.user)
  end

  # process the track measurements - called from cron job
  def process(email=false)
    measure_count = self.measures.count
    user_login =   User.find(self.user_id).try(:login)
    puts "processing track #{self.id} (#{measure_count} measurements) of #{user_login}..."
    logger.info "processing track #{self.id} (#{measure_count} measurements) of #{user_login}..."

    return if self.process_clean_and_index == true #stop processing if process_clean returns true (track was invalid and is deleted)
    logger.info "process_clean_and_index track #{self.id} done."

    self.process_base
    logger.info "processs_base done."

    #self.process_geo_correction if self.geolocated #disabled for now (needs bug fix in geo_features.rb + map data of other places than paris)
    #logger.info "geo correction done."

    # New version of noisetube does not support autotagging
    #self.process_automatic_tagging
    #logger.info "automatic tagging done."

    self.processed = true
    self.public = self.user.public

    #add measurement count here
    self.measures_size = measure_count

    #city
    self.city = self.user.city

    self.save

    # update last activity attribute
    self.user.last_activity_at=self.ends_at
    self.user.save

    unless self.user.city.nil?
      self.user.city.last_activity_at=self.ends_at
      self.user.city.save
    end

    begin
      UserMailer.delay.deliver_new_track(self) if email
      logger.info "Sent track ready email to #{email}"
    rescue => ex
      logger.error ("Error: Could not send email to #{email}. "<<ex.message)
    end


    logger.info "..Track #{self.id} by user #{user_login} processed."
  end

  # geo-correct the track. to check if this still works correctly
  def process_geo_correction
    GeoFeatures::correct_track(self)
  end

  # process_clean - removes the invalid measures, and checks if the track has
  # more than 3 measures after that and if so, deletes it
  def process_clean_and_index
    self.measures.each { |m|
      unless m.isvalid?
        m.destroy
      else
        m.loudness_index=SoundLevel.compute_loudness_index(m.loudness)
        m.time_index=Measure.compute_time_index(m)
        m.save
      end
    }
    if self.measures.size < LEAST_MEASURES_SIZE_PER_TRACK && self.user.id != 1395 #for dublin festival (users customised android app with hardcoded account, we expect people to make short tracks)
      logger.warn "Not enough (valid) measures => deleting track #{self.id}"
      self.destroy
      return true
    end
    return false
  end

  # process track helper
  def process_base

    # set starts_at to timestamp of first measurement
    self.starts_at = self.measures.first(:order=>"made_at").try(:made_at)
    
    # set ends_at to timestamp of last measurement
    self.ends_at = self.measures.find(:last, :order=>"made_at").try(:made_at)

    # number of measurements
    self.count = self.measures.size

    # duration
    #self.duration = self.ends_at - self.starts_at

    # avg loudness
    loudness_data=self.measures.collect{ |m| m.loudness }
    self.avgloudness=SoundLevel::compute_decibel(loudness_data)
    min = 1000000.0
    max = 0.0
    loudness_data.each { |leq|
      min = leq if leq < min
      max = leq if leq > max
    }
    self.minloudness = min
    self.maxloudness = max

    # graph computing
    self.graph_image_url=self.to_graph_url

    # location computing
    geodata = []
    # use the geodata to store bounding box and coverage, distance
    self.measures.all(:order=>"made_at").each { |m| geodata << m.geom unless m.geom.nil? }
    
    unless (geodata.size < 2)
      puts "Track is geolocated"
      self.geolocated = true

      # bounding box
      geom_shape = GeoFeatures::to_box(geodata)
      self.geom=geom_shape
      #self.geom.srid = 4326

      # coverage
      self.distance=GeoFeatures::coverage(geodata)

      # compute image overlay
      # Overlay.build_image(self.measures, "#{RAILS_ROOT}/public/images/tracks/#{self.id}.png")
      # #self.image_relative_url="/images/tracks/#{self.id}.png"
    else
      puts "Track is not geolocated"
      self.geolocated = false #just to be sure (and correct wrongly labeled tracks due to previous buggy postprocessing)
    end

    #find_location # find associated city geographically

    self.save #!!!
  end

  # deprecated todo: delete
  def process_automatic_tagging

    logger.info "Automatic tagging of track #{self.id}  (#{self.measures.count} measurements)"

    #Measure.update_all("tagged = NULL", "track_id=#{self.id}")

    last=self.measures.find(:last, :order=>"made_at")
    unless last.nil?

      # machine interpreter
      Interpreter.interpreters.each{ |interpreterclass|
        interpreter=interpreterclass.new
        #interpreter.process(self)
      }
      # mass update
      #Measure.update_all("tagged = true", "track_id=#{self.id}")
    end

  end

  def tags_count(context="tags")
    unless context.nil?
      context_sql= " and context='#{context}'"
    end
    Tagging.count(:joins=>"LEFT OUTER JOIN measures ON measures.id = taggings.taggable_id", :conditions=> "measures.track_id=#{self.id} #{context_sql}")
  end

  #get tags of context - slow
  def tags(limit=100, context="tags")
    MeasurementsTag.tag_counts_on(context, {:conditions=> "measurements_tags.track_id=#{self.id}", :order=>"created_at desc", :limit=>limit})
  end

  def popular_track_tags
      MeasurementsTag.select("tag_name, count(tag_name) as count")
        .where("track_id = #{self.id}").group("tag_name")
        .order("count(tag_name) desc").limit(5)
      #select tag_name, count(tag_name) from measurements_tags
      #where user_id =  52 group by tag_name
      #order by count(tag_name) desc
  end

  def to_graph_url(options={})

    unless self.graph_image_url.nil?
      return self.graph_image_url
    else
      # no graph url => compute it
      measures = (self.measures.size>2000) ? self.measures[0..1999] : self.measures
      graph = SoundLevel::graph(measures, options)

      # save only if the track has been processed
      if self.processed
        self.graph_image_url=graph
        self.save
      end

      return graph
    end
  end

  #process location of tracks
  def find_location

    measurement=self.measures.first(:conditions=>"geom is not null", :order=>"made_at")
    if measurement.nil?
      self.city=user.city if self.city.nil?
    else
      begin
        res = Geokit::Geocoders::GoogleGeocoder.reverse_geocode "#{measurement.geom.lat},#{measurement.geom.lng}"
        unless (res.nil?)
          self.location=res.full_address if (res.street_address.nil?)
          unless (res.city.nil?)
            self.city=City.find_or_build(res.city.downcase, res.country_code)
          else
            self.city=user.city
          end
        end
      rescue
        self.city=user.city
      end
    end
  end

  def geomeasures
    self.measures.where("geom is not NULL")  #track is for user, so no ned for user_id
  end

  # ---------- json serialization ------------
  def as_json(options = { })
    t = super(options.reverse_merge(:only => [:id, :user_id, :duration, :avg_loudness, :device]))
    t[:polygon] = ""
    unless !self.has_attribute?("geom")
      t[:polygon] = self.geom.try(:exterior_ring)
    end
    t
  end

end
